/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.foundation.text.selection

import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.ComposeFoundationFlags
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.contextmenu.ContextMenuScope
import androidx.compose.foundation.contextmenu.ContextMenuState
import androidx.compose.foundation.internal.checkPreconditionNotNull
import androidx.compose.foundation.internal.isAutofillAvailable
import androidx.compose.foundation.internal.isReadSupported
import androidx.compose.foundation.internal.isWriteSupported
import androidx.compose.foundation.internal.readAnnotatedString
import androidx.compose.foundation.internal.toClipEntry
import androidx.compose.foundation.text.DefaultCursorThickness
import androidx.compose.foundation.text.Handle
import androidx.compose.foundation.text.HandleState
import androidx.compose.foundation.text.HandleState.Cursor
import androidx.compose.foundation.text.HandleState.None
import androidx.compose.foundation.text.HandleState.Selection
import androidx.compose.foundation.text.LegacyTextFieldState
import androidx.compose.foundation.text.MenuItemsAvailability
import androidx.compose.foundation.text.TextContextMenuItems
import androidx.compose.foundation.text.TextContextMenuItems.Autofill
import androidx.compose.foundation.text.TextContextMenuItems.Copy
import androidx.compose.foundation.text.TextContextMenuItems.Cut
import androidx.compose.foundation.text.TextContextMenuItems.Paste
import androidx.compose.foundation.text.TextContextMenuItems.SelectAll
import androidx.compose.foundation.text.TextDragObserver
import androidx.compose.foundation.text.TextItem
import androidx.compose.foundation.text.UndoManager
import androidx.compose.foundation.text.ValidatingEmptyOffsetMappingIdentity
import androidx.compose.foundation.text.contextmenu.modifier.ToolbarRequester
import androidx.compose.foundation.text.contextmenu.modifier.ToolbarRequesterImpl
import androidx.compose.foundation.text.contextmenu.modifier.showTextContextMenuOnSecondaryClick
import androidx.compose.foundation.text.contextmenu.modifier.textContextMenuToolbarHandler
import androidx.compose.foundation.text.contextmenu.modifier.translateRootToDestination
import androidx.compose.foundation.text.detectDownAndDragGesturesWithObserver
import androidx.compose.foundation.text.getLineHeight
import androidx.compose.foundation.text.isPositionInsideSelection
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.TextToolbarStatus
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.input.getSelectedText
import androidx.compose.ui.text.input.getTextAfterSelection
import androidx.compose.ui.text.input.getTextBeforeSelection
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.launch

/** A bridge class between user interaction to the text field selection. */
internal class TextFieldSelectionManager(val undoManager: UndoManager? = null) {

    /** The current [OffsetMapping] for text field. */
    internal var offsetMapping: OffsetMapping = ValidatingEmptyOffsetMappingIdentity

    /** Called when the input service updates the values in [TextFieldValue]. */
    internal var onValueChange: (TextFieldValue) -> Unit = {}

    /** The current [LegacyTextFieldState]. */
    internal var state: LegacyTextFieldState? = null

    /**
     * The current [TextFieldValue]. This contains the original text, not the transformed text.
     * Transformed text can be found with [transformedText].
     */
    private val valueState = mutableStateOf(TextFieldValue())

    internal var value: TextFieldValue
        get() = valueState.value
        set(value) {
            valueState.value = value
            latestSelection = value.selection
        }

    /**
     * The current transformed text from the [LegacyTextFieldState]. The original text can be found
     * in [value].
     */
    internal val transformedText
        get() = state?.textDelegate?.text

    /**
     * Visual transformation of the text field's text. Used to check if certain toolbar options are
     * permitted. For example, 'cut' will not be available is it is password transformation.
     */
    internal var visualTransformation: VisualTransformation = VisualTransformation.None

    /** The action to invoke when autofill is requested in text toolbar. */
    internal var requestAutofillAction: (() -> Unit)? = null

    /** [Clipboard] to perform clipboard features. */
    internal var clipboard: Clipboard? = null

    /** [CoroutineScope] to perform clipboard features */
    internal var coroutineScope: CoroutineScope? = null

    internal var platformSelectionBehaviors: PlatformSelectionBehaviors? = null

    /** [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M). */
    var textToolbar: TextToolbar? = null

    /** [HapticFeedback] handle to perform haptic feedback. */
    var hapticFeedBack: HapticFeedback? = null

    /** [FocusRequester] used to request focus for the TextField. */
    var focusRequester: FocusRequester? = null

    /** Defines if paste and cut toolbar menu actions should be shown */
    var editable by mutableStateOf(true)

    /** Whether the text field should be selectable at all. */
    var enabled by mutableStateOf(true)

    /**
     * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
     * recalculated.
     */
    private var dragBeginPosition = Offset.Zero

    /**
     * The beginning selection of the drag gesture translated into position in text. Every time a
     * new long press and drag gesture starts, it wil be recalculated. It's null if 1) when there is
     * no ongoing drag gesture, or 2) when the drag gesture starts from an empty area in the text
     * field.
     */
    private var dragBeginSelection: TextRange? = null

    /**
     * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
     * it will be zeroed out.
     */
    private var dragTotalDistance = Offset.Zero

    /**
     * A flag to check if a selection or cursor handle is being dragged, and which handle is being
     * dragged. If this value is non-null, then onPress will not select any text. This value will be
     * set to non-null when either handle is being dragged, and be reset to null when the dragging
     * is stopped.
     */
    var draggingHandle: Handle? by mutableStateOf(null)
        private set

    /** The current position of a drag, in decoration box coordinates. */
    var currentDragPosition: Offset? by mutableStateOf(null)
        private set

    /**
     * The previous offset of a drag, before selection adjustments. Only update when a selection
     * layout change has occurred, or set to -1 if a new drag begins.
     */
    private var previousRawDragOffset: Int = -1

    /**
     * The old [TextFieldValue] before entering the selection mode on long press. Used to exit the
     * selection mode.
     */
    private var oldValue: TextFieldValue = TextFieldValue()

    /** The previous [SelectionLayout] where [SelectionLayout.shouldRecomputeSelection] was true. */
    private var previousSelectionLayout: SelectionLayout? = null

    /**
     * The latest selection range that was passed to [onValueChange]. The [value] state is updated
     * only after CoreTextField is recomposed. Even after [onValueChange] is called with a new
     * value, we won't know the final value until the next frame.
     *
     * **USE WITH CAUTION**: Be aware that developer can change selection in [onValueChange]. This
     * selection is our best guess, but is not guaranteed to be the same as the new [value]. This is
     * introduced for smart selection feature.
     */
    internal var latestSelection: TextRange? = null

    // TODO(grantapher) android ClipboardManager has a way to notify primary clip changes.
    //  That could possibly be used so that this doesn't have to be updated manually.
    /** The current availability of text for pasting. Updated via [updateClipboardEntry]. */
    private var hasAvailableTextToPaste by mutableStateOf(false)

    @VisibleForTesting internal var toolbarRequester: ToolbarRequester = ToolbarRequesterImpl()

    val contextMenuAreaModifier
        get() =
            if (!enabled) Modifier
            else
                Modifier.showTextContextMenuOnSecondaryClick(
                        onPreShowContextMenu = { clickLocation ->
                            updateClipboardEntry()
                            getContextTextAndSelection()?.let { (text, selection) ->
                                platformSelectionBehaviors?.onShowContextMenu(
                                    text = text,
                                    selection = selection,
                                    secondaryClickLocation = clickLocation,
                                )
                            }
                        }
                    )
                    .textContextMenuToolbarHandler(
                        requester = toolbarRequester,
                        onShow = {
                            updateClipboardEntry()
                            getContextTextAndSelection()?.let { (text, selection) ->
                                platformSelectionBehaviors?.onShowSelectionToolbar(text, selection)
                            }
                            textToolbarShownViaProvider = true
                        },
                        onHide = { textToolbarShownViaProvider = false },
                        computeContentBounds = { destinationCoordinates ->
                            val rootBounds = getContentRect()
                            val localCoordinates =
                                checkPreconditionNotNull(state?.layoutCoordinates)
                            translateRootToDestination(
                                rootContentBounds = rootBounds,
                                localCoordinates = localCoordinates,
                                destinationCoordinates = destinationCoordinates,
                            )
                        },
                    )

    /** [TextDragObserver] for long press and drag to select in TextField. */
    internal val touchSelectionObserver =
        object : TextDragObserver {
            private var isLongPressSelectionOnly = true
            private var runningSelection: TextRange? = null

            override fun onDown(point: Offset) {
                // Not supported for long-press-drag.
            }

            override fun onUp() {
                // Nothing to do.
            }

            override fun onStart(startPoint: Offset) {
                if (!enabled || draggingHandle != null) return
                // While selecting by long-press-dragging, the "end" of the selection is always the
                // one
                // being controlled by the drag.
                draggingHandle = Handle.SelectionEnd
                previousRawDragOffset = -1
                isLongPressSelectionOnly = true

                // ensuring that current action mode (selection toolbar) is invalidated
                hideSelectionToolbar()

                // Long Press at the blank area, the cursor should show up at the end of the line.
                if (state?.layoutResult?.isPositionOnText(startPoint) != true) {
                    state?.layoutResult?.let { layoutResult ->
                        val transformedOffset = layoutResult.getOffsetForPosition(startPoint)
                        val offset = offsetMapping.transformedToOriginal(transformedOffset)

                        val newValue =
                            createTextFieldValue(
                                annotatedString = value.annotatedString,
                                selection = TextRange(offset, offset),
                            )

                        enterSelectionMode(showFloatingToolbar = false)
                        hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
                        onValueChange(newValue)
                        latestSelection = newValue.selection
                    }
                    isLongPressSelectionOnly = false
                } else {
                    if (value.text.isEmpty()) return

                    enterSelectionMode(showFloatingToolbar = false)
                    val adjustedStartSelection =
                        updateSelection(
                            // reset selection, otherwise a previous selection may be used
                            // as context for creating the next selection
                            value = value.copy(selection = TextRange.Zero),
                            currentPosition = startPoint,
                            isStartOfSelection = true,
                            isStartHandle = false,
                            adjustment = SelectionAdjustment.Word,
                            isTouchBasedSelection = true,
                        )
                    // For touch, set the begin selection to the adjusted selection.
                    // When char based selection is used, we want to ensure we snap the
                    // beginning offset to the start word boundary of the first selected word.
                    dragBeginSelection = adjustedStartSelection
                    runningSelection = adjustedStartSelection
                }

                // don't set selection handle state until drag ends
                setHandleState(None)

                dragBeginPosition = startPoint
                currentDragPosition = dragBeginPosition
                dragTotalDistance = Offset.Zero
            }

            override fun onDrag(delta: Offset) {
                // selection never started, did not consume any drag
                if (!enabled || value.text.isEmpty()) return

                dragTotalDistance += delta
                state?.layoutResult?.let { layoutResult ->
                    currentDragPosition = dragBeginPosition + dragTotalDistance

                    val newSelection =
                        if (
                            dragBeginSelection == null &&
                                !layoutResult.isPositionOnText(currentDragPosition!!)
                        ) {
                            // both start and end of drag is in end padding.
                            val startOffset =
                                offsetMapping.transformedToOriginal(
                                    layoutResult.getOffsetForPosition(dragBeginPosition)
                                )

                            val endOffset =
                                offsetMapping.transformedToOriginal(
                                    layoutResult.getOffsetForPosition(currentDragPosition!!)
                                )

                            val adjustment =
                                if (startOffset == endOffset) {
                                    // start and end is in the same end padding, keep the collapsed
                                    // selection
                                    SelectionAdjustment.None
                                } else {
                                    SelectionAdjustment.Word
                                }

                            updateSelection(
                                value = value,
                                currentPosition = currentDragPosition!!,
                                isStartOfSelection = false,
                                isStartHandle = false,
                                adjustment = adjustment,
                                isTouchBasedSelection = true,
                            )
                        } else {
                            val startOffset =
                                dragBeginSelection?.start
                                    ?: layoutResult.getOffsetForPosition(
                                        position = dragBeginPosition,
                                        coerceInVisibleBounds = false,
                                    )
                            val endOffset =
                                layoutResult.getOffsetForPosition(
                                    position = currentDragPosition!!,
                                    coerceInVisibleBounds = false,
                                )

                            if (dragBeginSelection == null && startOffset == endOffset) {
                                // if we are selecting starting from end padding,
                                // don't start selection until we have and un-collapsed selection.
                                return
                            }

                            updateSelection(
                                value = value,
                                currentPosition = currentDragPosition!!,
                                isStartOfSelection = false,
                                isStartHandle = false,
                                adjustment = SelectionAdjustment.Word,
                                isTouchBasedSelection = true,
                            )
                        }

                    runningSelection = newSelection
                    if (newSelection != dragBeginSelection) {
                        isLongPressSelectionOnly = false
                    }
                }
                updateFloatingToolbar(show = false)
            }

            override fun onStop() = onEnd()

            override fun onCancel() = onEnd()

            private fun onEnd() {
                draggingHandle = null
                currentDragPosition = null
                updateFloatingToolbar(show = true)

                val collapsed = runningSelection?.collapsed ?: value.selection.collapsed
                setHandleState(if (collapsed) Cursor else Selection)
                state?.showSelectionHandleStart =
                    !collapsed && isSelectionHandleInVisibleBound(isStartHandle = true)
                state?.showSelectionHandleEnd =
                    !collapsed && isSelectionHandleInVisibleBound(isStartHandle = false)
                state?.showCursorHandle =
                    collapsed && isSelectionHandleInVisibleBound(isStartHandle = true)

                if (isLongPressSelectionOnly) {
                    // Note that even if we called onValueChange when selection is updated,
                    // value is only updated when the TextField is recomposed.
                    // So we have to use dragBeginSelection as the current selection.
                    maybeSuggestSelection(dragBeginSelection)
                }
                dragBeginSelection = null
            }
        }

    internal val mouseSelectionObserver =
        object : MouseSelectionObserver {
            var isDoubleOrTripleClickSelectionOnly = true
            var initialSelection: TextRange? = null

            override fun onExtend(downPosition: Offset): Boolean {
                // can't update selection without a layoutResult, so don't consume
                state?.layoutResult ?: return false
                if (!enabled) return false
                previousRawDragOffset = -1
                focusRequester?.requestFocus()
                updateMouseSelection(
                    value = value,
                    currentPosition = downPosition,
                    isStartOfSelection = false,
                    adjustment = SelectionAdjustment.None,
                )
                return true
            }

            override fun onExtendDrag(dragPosition: Offset): Boolean {
                if (!enabled || value.text.isEmpty()) return false
                // can't update selection without a layoutResult, so don't consume
                state?.layoutResult ?: return false

                updateMouseSelection(
                    value = value,
                    currentPosition = dragPosition,
                    isStartOfSelection = false,
                    adjustment = SelectionAdjustment.None,
                )
                return true
            }

            override fun onStart(
                downPosition: Offset,
                adjustment: SelectionAdjustment,
                clickCount: Int,
            ): Boolean {
                if (!enabled || value.text.isEmpty()) return false

                // can't update selection without a layoutResult, so don't consume
                state?.layoutResult ?: return false

                focusRequester?.requestFocus()
                dragBeginPosition = downPosition
                previousRawDragOffset = -1
                enterSelectionMode()
                val newSelection =
                    updateMouseSelection(
                        value = value,
                        currentPosition = dragBeginPosition,
                        isStartOfSelection = true,
                        adjustment = adjustment,
                    )
                if (clickCount >= 2) {
                    isDoubleOrTripleClickSelectionOnly = true
                    this.initialSelection = newSelection
                }
                return true
            }

            override fun onDrag(dragPosition: Offset, adjustment: SelectionAdjustment): Boolean {
                if (!enabled || value.text.isEmpty()) return false
                // can't update selection without a layoutResult, so don't consume
                state?.layoutResult ?: return false

                updateMouseSelection(
                    value = value,
                    currentPosition = dragPosition,
                    isStartOfSelection = false,
                    adjustment = adjustment,
                )
                return true
            }

            fun updateMouseSelection(
                value: TextFieldValue,
                currentPosition: Offset,
                isStartOfSelection: Boolean,
                adjustment: SelectionAdjustment,
            ): TextRange {
                val newSelection =
                    updateSelection(
                        value = value,
                        currentPosition = currentPosition,
                        isStartOfSelection = isStartOfSelection,
                        isStartHandle = false,
                        adjustment = adjustment,
                        isTouchBasedSelection = false,
                    )
                if (newSelection != initialSelection) {
                    isDoubleOrTripleClickSelectionOnly = false
                }
                setHandleState(if (newSelection.collapsed) Cursor else Selection)
                return newSelection
            }

            override fun onDragDone() {
                if (isDoubleOrTripleClickSelectionOnly) {
                    maybeSuggestSelection(initialSelection)
                }
            }
        }

    private fun maybeSuggestSelection(selection: TextRange?) {
        if (selection == null) return
        val platformSelectionBehaviors =
            this@TextFieldSelectionManager.platformSelectionBehaviors ?: return
        val text = transformedText?.text ?: return
        val offsetMapping = this@TextFieldSelectionManager.offsetMapping
        val transformedSelection =
            TextRange(
                offsetMapping.originalToTransformed(selection.start),
                offsetMapping.originalToTransformed(selection.end),
            )

        if (text.isNotEmpty() && !transformedSelection.collapsed) {
            coroutineScope?.launch {
                val newSelection =
                    platformSelectionBehaviors
                        .suggestSelectionForLongPressOrDoubleClick(text, transformedSelection)
                        ?.let {
                            TextRange(
                                offsetMapping.transformedToOriginal(it.start),
                                offsetMapping.transformedToOriginal(it.end),
                            )
                        } ?: return@launch
                if (
                    newSelection != selection &&
                        value.text == text &&
                        offsetMapping === this@TextFieldSelectionManager.offsetMapping
                ) {
                    onValueChange(
                        createTextFieldValue(
                            annotatedString = value.annotatedString,
                            selection = newSelection,
                        )
                    )
                    latestSelection = newSelection
                }
            }
        }
    }

    /**
     * [TextDragObserver] for dragging the selection handles to change the selection in TextField.
     */
    internal fun handleDragObserver(isStartHandle: Boolean): TextDragObserver =
        object : TextDragObserver {
            override fun onDown(point: Offset) {
                draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd

                // The position of the character where the drag gesture should begin. This is in
                // the inner text field coordinates.
                val handleCoordinates = getAdjustedCoordinates(getHandlePosition(isStartHandle))

                // translate to decoration box coordinates
                val layoutResult = state?.layoutResult ?: return
                val translatedPosition =
                    layoutResult.translateInnerToDecorationCoordinates(handleCoordinates)

                dragBeginPosition = translatedPosition
                currentDragPosition = translatedPosition

                // Zero out the total distance that being dragged.
                dragTotalDistance = Offset.Zero
                previousRawDragOffset = -1

                state?.isInTouchMode = true
                updateFloatingToolbar(show = false)
            }

            override fun onUp() {
                draggingHandle = null
                currentDragPosition = null
                updateFloatingToolbar(show = true)
            }

            override fun onStart(startPoint: Offset) {
                // handled in onDown
            }

            override fun onDrag(delta: Offset) {
                dragTotalDistance += delta

                currentDragPosition = dragBeginPosition + dragTotalDistance
                updateSelection(
                    value = value,
                    currentPosition = currentDragPosition!!,
                    isStartOfSelection = false,
                    isStartHandle = isStartHandle,
                    adjustment = SelectionAdjustment.CharacterWithWordAccelerate,
                    isTouchBasedSelection = true, // handle drag infers touch
                )
                updateFloatingToolbar(show = false)
            }

            override fun onStop() {
                draggingHandle = null
                currentDragPosition = null
                updateFloatingToolbar(show = true)
            }

            override fun onCancel() {}
        }

    /** [TextDragObserver] for dragging the cursor to change the selection in TextField. */
    internal fun cursorDragObserver(): TextDragObserver =
        object : TextDragObserver {
            override fun onDown(point: Offset) {
                // Nothing
            }

            override fun onUp() {
                draggingHandle = null
                currentDragPosition = null
            }

            override fun onStart(startPoint: Offset) {
                // The position of the character where the drag gesture should begin. This is in
                // the inner text field coordinates.
                val handleCoordinates = getAdjustedCoordinates(getHandlePosition(true))

                // translate to decoration box coordinates
                val layoutResult = state?.layoutResult ?: return
                val translatedPosition =
                    layoutResult.translateInnerToDecorationCoordinates(handleCoordinates)

                dragBeginPosition = translatedPosition
                currentDragPosition = translatedPosition
                // Zero out the total distance that being dragged.
                dragTotalDistance = Offset.Zero
                draggingHandle = Handle.Cursor
                updateFloatingToolbar(show = false)
            }

            override fun onDrag(delta: Offset) {
                dragTotalDistance += delta

                state?.layoutResult?.let { layoutResult ->
                    currentDragPosition = dragBeginPosition + dragTotalDistance
                    val offset =
                        offsetMapping.transformedToOriginal(
                            layoutResult.getOffsetForPosition(currentDragPosition!!)
                        )

                    val newSelection = TextRange(offset, offset)

                    // Nothing changed, skip onValueChange hand hapticFeedback.
                    if (newSelection == value.selection) return

                    if (state?.isInTouchMode != false) {
                        hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
                    }

                    onValueChange(
                        createTextFieldValue(
                            annotatedString = value.annotatedString,
                            selection = newSelection,
                        )
                    )
                    latestSelection = newSelection
                }
            }

            override fun onStop() {
                draggingHandle = null
                currentDragPosition = null
            }

            override fun onCancel() {}
        }

    /**
     * The method to record the required state values on entering the selection mode.
     *
     * Is triggered on long press or accessibility action.
     *
     * @param showFloatingToolbar whether to show the floating toolbar when entering selection mode
     */
    internal fun enterSelectionMode(showFloatingToolbar: Boolean = true) {
        if (state?.hasFocus == false) {
            focusRequester?.requestFocus()
        }
        oldValue = value
        updateFloatingToolbar(showFloatingToolbar)
        setHandleState(Selection)
    }

    /**
     * The method to record the corresponding state values on exiting the selection mode.
     *
     * Is triggered on accessibility action.
     */
    internal fun exitSelectionMode() {
        updateFloatingToolbar(show = false)
        setHandleState(None)
    }

    internal fun deselect(position: Offset? = null) {
        if (!value.selection.collapsed) {
            // if selection was not collapsed, set a default cursor location, otherwise
            // don't change the location of the cursor.
            val layoutResult = state?.layoutResult
            val newCursorOffset =
                if (position != null && layoutResult != null) {
                    offsetMapping.transformedToOriginal(layoutResult.getOffsetForPosition(position))
                } else {
                    value.selection.max
                }
            val newValue = value.copy(selection = TextRange(newCursorOffset))
            onValueChange(newValue)
            latestSelection = newValue.selection
        }

        // If a new cursor position is given and the text is not empty, enter the Cursor state.
        val selectionMode = if (position != null && value.text.isNotEmpty()) Cursor else None
        setHandleState(selectionMode)
        updateFloatingToolbar(show = false)
    }

    internal fun setSelectionPreviewHighlight(range: TextRange) {
        state?.selectionPreviewHighlightRange = range
        state?.deletionPreviewHighlightRange = TextRange.Zero
        if (!range.collapsed) exitSelectionMode()
    }

    internal fun setDeletionPreviewHighlight(range: TextRange) {
        state?.deletionPreviewHighlightRange = range
        state?.selectionPreviewHighlightRange = TextRange.Zero
        if (!range.collapsed) exitSelectionMode()
    }

    internal fun clearPreviewHighlight() {
        state?.deletionPreviewHighlightRange = TextRange.Zero
        state?.selectionPreviewHighlightRange = TextRange.Zero
    }

    internal var textToolbarShownViaProvider = false

    @OptIn(ExperimentalFoundationApi::class)
    internal val textToolbarShown
        get() =
            if (ComposeFoundationFlags.isNewContextMenuEnabled) {
                textToolbarShownViaProvider
            } else {
                textToolbar?.status == TextToolbarStatus.Shown
            }

    private val isPassword: Boolean
        get() = visualTransformation is PasswordVisualTransformation

    private val hasSelection: Boolean
        get() = !value.selection.collapsed

    internal fun canCopy(): Boolean =
        hasSelection && !isPassword && clipboard?.isWriteSupported() == true

    internal suspend fun updateClipboardEntry() {
        if (clipboard?.isReadSupported() == true) {
            hasAvailableTextToPaste = hasAvailableTextToPaste()
        }
    }

    private fun getContextTextAndSelection(): Pair<String, TextRange>? {
        val text = transformedText?.text ?: return null
        val selection =
            latestSelection?.let { selection ->
                TextRange(
                    offsetMapping.originalToTransformed(selection.start),
                    offsetMapping.originalToTransformed(selection.end),
                )
            } ?: return null

        return Pair(text, selection)
    }

    /** Only fully accurate if [updateClipboardEntry] has been called. */
    internal fun canPaste(): Boolean =
        editable && hasAvailableTextToPaste && clipboard?.isReadSupported() == true

    internal fun canCut(): Boolean =
        hasSelection && editable && !isPassword && clipboard?.isWriteSupported() == true

    internal fun canSelectAll(): Boolean = value.selection.length != value.text.length

    internal fun canAutofill(): Boolean = editable && value.selection.collapsed

    /**
     * The method for copying text.
     *
     * If there is no selection, return. Put the selected text into the [Clipboard], and cancel the
     * selection, if [cancelSelection] is true. The text in the text field should be unchanged. If
     * [cancelSelection] is true, the new cursor offset should be at the end of the previous
     * selected text.
     */
    internal fun copy(cancelSelection: Boolean = true) =
        coroutineScope?.launch(start = CoroutineStart.UNDISPATCHED) {
            if (value.selection.collapsed) return@launch

            // TODO(b/171947959) check if original or transformed should be copied
            clipboard?.setClipEntry(value.getSelectedText().toClipEntry())

            if (!cancelSelection) return@launch

            val newCursorOffset = value.selection.max
            val newValue =
                createTextFieldValue(
                    annotatedString = value.annotatedString,
                    selection = TextRange(newCursorOffset, newCursorOffset),
                )
            onValueChange(newValue)
            latestSelection = newValue.selection
            setHandleState(None)
        }

    /**
     * The method for pasting text.
     *
     * Get the text from [Clipboard]. If it's null, return. The new text should be the text before
     * the selected text, plus the text from the [Clipboard], and plus the text after the selected
     * text. Then the selection should collapse, and the new cursor offset should be the end of the
     * newly added text.
     */
    internal fun paste() =
        coroutineScope?.launch(start = CoroutineStart.UNDISPATCHED) {
            val text = clipboard?.getClipEntry()?.readAnnotatedString() ?: return@launch

            val newText =
                value.getTextBeforeSelection(value.text.length) +
                    text +
                    value.getTextAfterSelection(value.text.length)
            val newCursorOffset = value.selection.min + text.length

            val newValue =
                createTextFieldValue(
                    annotatedString = newText,
                    selection = TextRange(newCursorOffset, newCursorOffset),
                )
            onValueChange(newValue)
            latestSelection = newValue.selection
            setHandleState(None)
            undoManager?.forceNextSnapshot()
        }

    /**
     * The method for cutting text.
     *
     * If there is no selection, return. Put the selected text into the [Clipboard]. The new text
     * should be the text before the selection plus the text after the selection. And the new cursor
     * offset should be between the text before the selection, and the text after the selection.
     */
    internal fun cut() =
        coroutineScope?.launch(start = CoroutineStart.UNDISPATCHED) {
            if (value.selection.collapsed) return@launch

            // TODO(b/171947959) check if original or transformed should be cut
            clipboard?.setClipEntry(value.getSelectedText().toClipEntry())

            val newText =
                value.getTextBeforeSelection(value.text.length) +
                    value.getTextAfterSelection(value.text.length)
            val newCursorOffset = value.selection.min

            val newValue =
                createTextFieldValue(
                    annotatedString = newText,
                    selection = TextRange(newCursorOffset, newCursorOffset),
                )
            onValueChange(newValue)
            latestSelection = newValue.selection
            setHandleState(None)
            undoManager?.forceNextSnapshot()
        }

    /*@VisibleForTesting*/
    internal fun selectAll() {
        val newValue =
            createTextFieldValue(
                annotatedString = value.annotatedString,
                selection = TextRange(0, value.text.length),
            )
        onValueChange(newValue)
        latestSelection = newValue.selection
        oldValue = oldValue.copy(selection = newValue.selection)
        enterSelectionMode(showFloatingToolbar = true)
    }

    internal fun autofill() {
        requestAutofillAction?.invoke()
    }

    internal fun getHandlePosition(isStartHandle: Boolean): Offset {
        val textLayoutResult = state?.layoutResult?.value ?: return Offset.Unspecified

        // If layout and value are out of sync, return unspecified.
        // This will be called again once they are in sync.
        val transformedText = transformedText ?: return Offset.Unspecified
        val layoutInputText = textLayoutResult.layoutInput.text.text
        if (transformedText.text != layoutInputText) return Offset.Unspecified

        val offset = if (isStartHandle) value.selection.start else value.selection.end
        return getSelectionHandleCoordinates(
            textLayoutResult = textLayoutResult,
            offset = offsetMapping.originalToTransformed(offset),
            isStart = isStartHandle,
            areHandlesCrossed = value.selection.reversed,
        )
    }

    internal fun getHandleLineHeight(isStartHandle: Boolean): Float {
        val offset = if (isStartHandle) value.selection.start else value.selection.end
        return state?.layoutResult?.value?.getLineHeight(offset) ?: return 0f
    }

    internal fun getCursorPosition(density: Density): Offset {
        val offset = offsetMapping.originalToTransformed(value.selection.start)
        val layoutResult = state?.layoutResult!!.value
        val cursorRect =
            layoutResult.getCursorRect(offset.coerceIn(0, layoutResult.layoutInput.text.length))
        val x = with(density) { cursorRect.left + DefaultCursorThickness.toPx() / 2 }
        return Offset(x, cursorRect.bottom)
    }

    /**
     * Update the [LegacyTextFieldState.showFloatingToolbar] state and show/hide the toolbar.
     *
     * You may want to call [showSelectionToolbar] and [hideSelectionToolbar] directly without
     * updating the [LegacyTextFieldState.showFloatingToolbar] if you are simply hiding all touch
     * selection behaviors (toolbar, handles, cursor, magnifier), but want the toolbar to come back
     * when you un-hide all those behaviors.
     */
    private fun updateFloatingToolbar(show: Boolean) {
        state?.showFloatingToolbar = show
        if (show) showSelectionToolbar() else hideSelectionToolbar()
    }

    @OptIn(ExperimentalFoundationApi::class)
    internal fun showSelectionToolbar() {
        // Because this is called once in CoreTextField composition,
        // disable read observation to avoid reading states and landing in a composition loop.
        Snapshot.withoutReadObservation { if (!enabled || state?.isInTouchMode == false) return }

        if (ComposeFoundationFlags.isNewContextMenuEnabled) {
            toolbarRequester.show()
        } else {
            showSelectionToolbarViaTextToolbar()
        }
    }

    /**
     * This function get the selected region as a Rectangle region, and pass it to [TextToolbar] to
     * make the FloatingToolbar show up in the proper place. In addition, this function passes the
     * copy, paste and cut method as callbacks when "copy", "cut" or "paste" is clicked.
     */
    private fun showSelectionToolbarViaTextToolbar() =
        coroutineScope?.launch(start = CoroutineStart.UNDISPATCHED) {
            updateClipboardEntry()

            // Because this is undispatched and the above is called once in CoreTextField
            // composition, disable read observation to avoid reading many states and landing
            // in a composition loop.
            Snapshot.withoutReadObservation {
                val copy: (() -> Unit)? =
                    if (canCopy()) {
                        {
                            coroutineScope?.launch(start = CoroutineStart.UNDISPATCHED) { copy() }
                            hideSelectionToolbar()
                        }
                    } else null

                val cut: (() -> Unit)? =
                    if (canCut()) {
                        {
                            coroutineScope?.launch(start = CoroutineStart.UNDISPATCHED) { cut() }
                            hideSelectionToolbar()
                        }
                    } else null

                val paste: (() -> Unit)? =
                    if (canPaste()) {
                        {
                            coroutineScope?.launch(start = CoroutineStart.UNDISPATCHED) { paste() }
                            hideSelectionToolbar()
                        }
                    } else null

                val selectAll: (() -> Unit)? =
                    if (canSelectAll()) {
                        { selectAll() }
                    } else null

                val autofill: (() -> Unit)? =
                    if (canAutofill()) {
                        { autofill() }
                    } else null

                textToolbar?.showMenu(
                    rect = getContentRect(),
                    onCopyRequested = copy,
                    onPasteRequested = paste,
                    onCutRequested = cut,
                    onSelectAllRequested = selectAll,
                    onAutofillRequested = autofill,
                )
            }
        }

    @OptIn(ExperimentalFoundationApi::class)
    internal fun hideSelectionToolbar() {
        if (ComposeFoundationFlags.isNewContextMenuEnabled) {
            toolbarRequester.hide()
        } else {
            if (textToolbar?.status == TextToolbarStatus.Shown) {
                textToolbar?.hide()
            }
        }
    }

    /**
     * Implements the macOS select-word-on-right-click behavior.
     *
     * If the current selection does not already include [position], select the word at [position].
     */
    fun selectWordAtPositionIfNotAlreadySelected(position: Offset) {
        val layoutResult = state?.layoutResult ?: return
        val isClickedPositionInsideSelection =
            layoutResult.value.isPositionInsideSelection(
                position = layoutResult.translateDecorationToInnerCoordinates(position),
                selectionRange = value.selection,
            )
        if (!isClickedPositionInsideSelection) {
            updateSelection(
                value = value,
                currentPosition = position,
                isStartOfSelection = true,
                isStartHandle = false,
                adjustment = SelectionAdjustment.Word,
                isTouchBasedSelection = false,
            )
        }
    }

    /**
     * Check if the text in the text field changed. When the content in the text field is modified,
     * this method returns true.
     */
    internal fun isTextChanged(): Boolean {
        return oldValue.text != value.text
    }

    /**
     * Calculate selected region as [Rect]. The top is the top of the first selected line, and the
     * bottom is the bottom of the last selected line. The left is the leftmost handle's horizontal
     * coordinates, and the right is the rightmost handle's coordinates.
     */
    private fun getContentRect(): Rect {
        // if it's stale layout, return empty Rect
        state
            ?.takeIf { !it.isLayoutResultStale }
            ?.let {
                // value.selection is from the original representation.
                // we need to convert original offsets into transformed offsets to query
                // layoutResult because layoutResult belongs to the transformed text.
                val transformedStart = offsetMapping.originalToTransformed(value.selection.start)
                val transformedEnd = offsetMapping.originalToTransformed(value.selection.end)
                val startOffset =
                    state?.layoutCoordinates?.localToRoot(getHandlePosition(true)) ?: Offset.Zero
                val endOffset =
                    state?.layoutCoordinates?.localToRoot(getHandlePosition(false)) ?: Offset.Zero
                val startTop =
                    state
                        ?.layoutCoordinates
                        ?.localToRoot(
                            Offset(
                                0f,
                                it.layoutResult?.value?.getCursorRect(transformedStart)?.top ?: 0f,
                            )
                        )
                        ?.y ?: 0f
                val endTop =
                    state
                        ?.layoutCoordinates
                        ?.localToRoot(
                            Offset(
                                x = 0f,
                                y = it.layoutResult?.value?.getCursorRect(transformedEnd)?.top ?: 0f,
                            )
                        )
                        ?.y ?: 0f

                val left = min(startOffset.x, endOffset.x)
                val right = max(startOffset.x, endOffset.x)
                val top = min(startTop, endTop)
                val bottom =
                    max(startOffset.y, endOffset.y) + 25.dp.value * it.textDelegate.density.density

                return Rect(left, top, right, bottom)
            }

        return Rect.Zero
    }

    /**
     * Update the text field's selection based on new offsets.
     *
     * @param value the current [TextFieldValue]
     * @param currentPosition the current position of the cursor/drag in the decoration box
     *   coordinates
     * @param isStartOfSelection whether this is the first updateSelection of a selection gesture.
     *   If true, will ignore any previous selection context.
     * @param isStartHandle whether the start handle is being updated
     * @param adjustment The selection adjustment to use
     * @param isTouchBasedSelection Whether this is a touch based selection
     */
    private fun updateSelection(
        value: TextFieldValue,
        currentPosition: Offset,
        isStartOfSelection: Boolean,
        isStartHandle: Boolean,
        adjustment: SelectionAdjustment,
        isTouchBasedSelection: Boolean,
    ): TextRange {
        val layoutResult = state?.layoutResult ?: return TextRange.Zero
        val previousTransformedSelection =
            TextRange(
                offsetMapping.originalToTransformed(value.selection.start),
                offsetMapping.originalToTransformed(value.selection.end),
            )

        val currentOffset =
            layoutResult.getOffsetForPosition(
                position = currentPosition,
                coerceInVisibleBounds = false,
            )

        val rawStartHandleOffset =
            if (isStartHandle || isStartOfSelection) currentOffset
            else previousTransformedSelection.start

        val rawEndHandleOffset =
            if (!isStartHandle || isStartOfSelection) currentOffset
            else previousTransformedSelection.end

        val previousSelectionLayout = previousSelectionLayout // for smart cast
        val rawPreviousHandleOffset =
            if (
                isStartOfSelection || previousSelectionLayout == null || previousRawDragOffset == -1
            ) {
                -1
            } else {
                previousRawDragOffset
            }

        val selectionLayout =
            getTextFieldSelectionLayout(
                layoutResult = layoutResult.value,
                rawStartHandleOffset = rawStartHandleOffset,
                rawEndHandleOffset = rawEndHandleOffset,
                rawPreviousHandleOffset = rawPreviousHandleOffset,
                previousSelectionRange = previousTransformedSelection,
                isStartOfSelection = isStartOfSelection,
                isStartHandle = isStartHandle,
            )

        if (!selectionLayout.shouldRecomputeSelection(previousSelectionLayout)) {
            return value.selection
        }

        this.previousSelectionLayout = selectionLayout
        previousRawDragOffset = currentOffset

        val newTransformedSelection = adjustment.adjust(selectionLayout)
        val newSelection =
            TextRange(
                start = offsetMapping.transformedToOriginal(newTransformedSelection.start.offset),
                end = offsetMapping.transformedToOriginal(newTransformedSelection.end.offset),
            )

        if (newSelection == value.selection) return value.selection

        val onlyChangeIsReversed =
            newSelection.reversed != value.selection.reversed &&
                with(newSelection) { TextRange(end, start) } == value.selection

        val bothSelectionsCollapsed = newSelection.collapsed && value.selection.collapsed
        if (
            isTouchBasedSelection &&
                value.text.isNotEmpty() &&
                !onlyChangeIsReversed &&
                !bothSelectionsCollapsed
        ) {
            hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
        }

        val newValue =
            createTextFieldValue(annotatedString = value.annotatedString, selection = newSelection)
        onValueChange(newValue)
        latestSelection = newSelection

        if (!isTouchBasedSelection) {
            updateFloatingToolbar(show = !newSelection.collapsed)
        }

        state?.isInTouchMode = isTouchBasedSelection

        // showSelectionHandleStart/End might be set to false when scrolled out of the view.
        // When the selection is updated, they must also be updated so that handles will be shown
        // or hidden correctly.
        state?.showSelectionHandleStart =
            !newSelection.collapsed && isSelectionHandleInVisibleBound(isStartHandle = true)
        state?.showSelectionHandleEnd =
            !newSelection.collapsed && isSelectionHandleInVisibleBound(isStartHandle = false)
        state?.showCursorHandle =
            newSelection.collapsed && isSelectionHandleInVisibleBound(isStartHandle = true)

        return newSelection
    }

    private fun setHandleState(handleState: HandleState) {
        state?.takeUnless { it.handleState == handleState }?.let { it.handleState = handleState }
    }

    private fun createTextFieldValue(
        annotatedString: AnnotatedString,
        selection: TextRange,
    ): TextFieldValue {
        return TextFieldValue(annotatedString = annotatedString, selection = selection)
    }
}

@Composable
internal fun TextFieldSelectionHandle(
    isStartHandle: Boolean,
    direction: ResolvedTextDirection,
    manager: TextFieldSelectionManager,
) {
    val observer = remember(isStartHandle, manager) { manager.handleDragObserver(isStartHandle) }

    SelectionHandle(
        offsetProvider = { manager.getHandlePosition(isStartHandle) },
        isStartHandle = isStartHandle,
        direction = direction,
        handlesCrossed = manager.value.selection.reversed,
        lineHeight = manager.getHandleLineHeight(isStartHandle),
        modifier =
            Modifier.pointerInput(observer) { detectDownAndDragGesturesWithObserver(observer) },
    )
}

/** Whether the selection handle is in the visible bound of the TextField. */
internal fun TextFieldSelectionManager.isSelectionHandleInVisibleBound(
    isStartHandle: Boolean
): Boolean =
    state?.layoutCoordinates?.visibleBounds()?.containsInclusive(getHandlePosition(isStartHandle))
        ?: false

/**
 * Optionally shows a magnifier widget, if the current platform supports it, for the current state
 * of a [TextFieldSelectionManager]. Should check [TextFieldSelectionManager.draggingHandle] to see
 * which handle is being dragged and then calculate the magnifier position for that handle.
 *
 * Actual implementations should as much as possible actually live in this common source set, _not_
 * the platform-specific source sets. The actual implementations of this function should then just
 * delegate to those functions.
 */
internal expect fun Modifier.textFieldMagnifier(manager: TextFieldSelectionManager): Modifier

/** @return the location of the magnifier relative to the inner text field coordinates */
internal fun calculateSelectionMagnifierCenterAndroid(
    manager: TextFieldSelectionManager,
    magnifierSize: IntSize,
): Offset {
    // state read of currentDragPosition so that we always recompose on drag position changes
    val localDragPosition = manager.currentDragPosition ?: return Offset.Unspecified

    // Never show the magnifier in an empty text field.
    if (manager.transformedText?.isEmpty() != false) return Offset.Unspecified
    val rawTextOffset =
        when (manager.draggingHandle) {
            null -> return Offset.Unspecified
            Handle.Cursor,
            Handle.SelectionStart -> manager.value.selection.start
            Handle.SelectionEnd -> manager.value.selection.end
        }
    // If the text hasn't been laid out yet, don't show the magnifier.
    val textLayoutResultProxy = manager.state?.layoutResult ?: return Offset.Unspecified
    val transformedText = manager.state?.textDelegate?.text ?: return Offset.Unspecified

    val textOffset =
        manager.offsetMapping
            .originalToTransformed(rawTextOffset)
            .coerceIn(0, transformedText.length)

    val dragX = textLayoutResultProxy.translateDecorationToInnerCoordinates(localDragPosition).x

    val layoutResult = textLayoutResultProxy.value
    val line = layoutResult.getLineForOffset(textOffset)
    val lineStart = layoutResult.getLineLeft(line)
    val lineEnd = layoutResult.getLineRight(line)
    val lineMin = minOf(lineStart, lineEnd)
    val lineMax = maxOf(lineStart, lineEnd)
    val centerX = dragX.coerceIn(lineMin, lineMax)

    // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
    // magnifier actually is). See
    // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
    // Also check whether magnifierSize is calculated. A platform magnifier instance is not
    // created until it's requested for the first time. So the size will only be calculated after we
    // return a specified offset from this function.
    // It is very unlikely that this behavior would cause a flicker since magnifier immediately
    // shows up where the pointer is being dragged. The pointer needs to drag further than the half
    // of magnifier's width to hide by the following logic.
    if (
        magnifierSize != IntSize.Zero && (dragX - centerX).absoluteValue > magnifierSize.width / 2
    ) {
        return Offset.Unspecified
    }

    // Center vertically on the current line.
    val top = layoutResult.getLineTop(line)
    val bottom = layoutResult.getLineBottom(line)
    val centerY = ((bottom - top) / 2) + top

    return Offset(centerX, centerY)
}

internal expect fun Modifier.addBasicTextFieldTextContextMenuComponents(
    manager: TextFieldSelectionManager,
    coroutineScope: CoroutineScope,
): Modifier

internal fun TextFieldSelectionManager.contextMenuBuilder(
    contextMenuState: ContextMenuState,
    itemsAvailability: State<MenuItemsAvailability>,
): ContextMenuScope.() -> Unit = {
    fun textFieldItem(label: TextContextMenuItems, enabled: Boolean, operation: () -> Unit) {
        TextItem(contextMenuState, label, enabled, operation)
    }

    val availability: MenuItemsAvailability = itemsAvailability.value
    textFieldItem(Cut, enabled = availability.canCut) { cut() }
    textFieldItem(Copy, enabled = availability.canCopy) { copy(cancelSelection = false) }
    textFieldItem(Paste, enabled = availability.canPaste) { paste() }
    textFieldItem(SelectAll, enabled = availability.canSelectAll) { selectAll() }
    if (isAutofillAvailable()) {
        textFieldItem(Autofill, enabled = availability.canAutofill) { autofill() }
    }
}

/**
 * This method checks if it makes sense to show the "Paste" item in the context menu based on the
 * state of the [TextFieldSelectionManager.clipboard]. The returned value might be not enough -
 * there can be other conditions affecting the necessity for the "Paste" item.
 *
 * Note: Currently on web it will always return true. This mitigates the UX issue when a Clipboard
 * read permission is requested when a user has no intention to 'Paste' (e.g. before the context
 * menu is shown, regardless of "Paste" possibility). The downside is that it will show the 'Paste'
 * item even when there is nothing to paste. But we consider it to be a better Context Menu /
 * Toolbar UX than the alternative.
 */
internal expect suspend fun TextFieldSelectionManager.hasAvailableTextToPaste(): Boolean
